﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Net
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;

    /// <summary>Implements a fake server that sends predetermined responses upon receiving expected commands.</summary>
    internal sealed class FakeServer : IDisposable
    {
        private readonly int port;
        private readonly Encoding encoding;
        private readonly Action<NetworkStream> connectCallback;
        private readonly Queue<Command> commandQueue = new Queue<Command>();
        private readonly ManualResetEvent queueNonEmpty = new ManualResetEvent(false);
        private readonly Thread serverThread;
        private TcpClient tcpClient;
        private NetworkStream stream;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Terminates the server.</summary>
        public void Dispose()
        {
            this.serverThread.Abort();
            this.serverThread.Join();
            ((IDisposable)this.queueNonEmpty).Dispose();

            if (this.stream != null)
            {
                this.stream.Dispose();
            }

            if (this.tcpClient != null)
            {
                this.tcpClient.Close();
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal FakeServer(int port, Encoding encoding, Action<NetworkStream> connectCallback)
        {
            this.port = port;
            this.encoding = encoding;
            this.connectCallback = connectCallback;
            this.serverThread = new Thread(this.ThreadLoop);
            this.serverThread.Start();
        }

        internal void QueueCommand(byte[] expectedCommand, byte[] response)
        {
            lock (this.commandQueue)
            {
                this.commandQueue.Enqueue(new Command(expectedCommand, response));
                this.queueNonEmpty.Set();
            }
        }

        internal void QueueCommand(string expectedCommand, string response)
        {
            this.QueueCommand(this.encoding.GetBytes(expectedCommand), this.encoding.GetBytes(response));
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void ThreadLoop()
        {
            TcpListener listener = new TcpListener(IPAddress.Parse("127.0.0.1"), this.port);

            try
            {
                try
                {
                    listener.Start();

                    // Thread.Abort does not seem to work when we're inside and AcceptTcpClient() blocking call ...
                    while (!listener.Pending())
                    {
                        Thread.Sleep(300);
                    }

                    this.tcpClient = listener.AcceptTcpClient();
                }
                finally
                {
                    listener.Stop();
                }

                this.stream = this.tcpClient.GetStream();
                this.connectCallback(this.stream);

                while (true)
                {
                    Command command = this.DequeueCommand();
                    byte[] actualCommand = new byte[command.ExpectedCommand.Length];
                    int offset = 0;

                    // If the actual command is shorter than the expected one, this loop ensures that the thread is
                    // blocking here and that the client will not receive a response
                    while ((offset += this.stream.Read(actualCommand, offset, actualCommand.Length - offset)) <
                        actualCommand.Length)
                    {
                    }

                    if (command.ExpectedCommand.SequenceEqual(actualCommand))
                    {
                        this.stream.Write(command.Response, 0, command.Response.Length);
                    }
                    else
                    {
                        // If the actual command does not match the expected command, breaking out of the loop (and thus
                        // terminating the thread) ensures that the client will not receive a response and time out.
                        break;
                    }

                    // If the actual command is longer than the expected command, we'll detect this when the next command is
                    // processed
                }
            }
            catch (ThreadAbortException)
            {
            }
        }

        private Command DequeueCommand()
        {
            while (true)
            {
                this.queueNonEmpty.WaitOne();

                lock (this.commandQueue)
                {
                    if (this.commandQueue.Count > 0)
                    {
                        return this.commandQueue.Dequeue();
                    }
                    else
                    {
                        this.queueNonEmpty.Reset();
                    }
                }
            }
        }

        /// <summary>Encapsulates an expected command and its response.</summary>
        private struct Command
        {
            internal readonly byte[] ExpectedCommand;
            internal readonly byte[] Response;

            internal Command(byte[] expectedCommand, byte[] response)
            {
                this.ExpectedCommand = expectedCommand;
                this.Response = response;
            }
        }
    }
}
